package com.crunch.physics;

import com.crunch.math.Vector2f;

/**
 * A movable entity in the physics simulation.
 */
public class MovablePhysicsEntity {
	public static class Description {
		public Polygon shape;
		public float rotation;
		public Vector2f initialPosition;
		public Vector2f initialVelocity;
		public float maxSquishAngle; // radians
		public Object data;
		public EventHandler eventHandler;
	}

	public interface EventHandler {
		// called when a dynamic entity is "squished" by a movable entity
		// note: this function should NOT perform any immediate actions except for adjusting the position/velocity
		// of the dynamic entity being acted upon - e.g. objects should not be added or removed, and the movable
		// entity should not be moved
		// this is because this function is called in the middle of a simulation step, and not all time-ordered events
		// are processed sequentially, so the simulation may be in an "inconsistent" state
		// movableEntity's and dynamicEntity's position and velocity are guaranteed to be correct however
		public void onSquish(MovablePhysicsEntity movableEntity, DynamicPhysicsEntity dynamicEntity);
	}

	MovablePhysicsEntity(PhysicsSimulation simulation, Description description) {
		if (description.shape == null) {
			throw new IllegalArgumentException("null shape");
		}
		if (!description.shape.isConvex()) {
			throw new IllegalArgumentException("Non-convex shape");
		}
		if (description.maxSquishAngle < 0.0f) {
			throw new IllegalArgumentException("Invalid max squish angle");
		}

		this.simulation = simulation;

		shape = description.shape;
		rotation = description.rotation % (2.0f * (float) Math.PI);
		if (rotation < 0.0f) {
			rotation += 2.0f * (float) Math.PI;
		}
		rotationVector = new Vector2f((float) Math.cos(rotation), (float) Math.sin(rotation));
		position = new Vector2f(description.initialPosition);
		velocity = new Vector2f(description.initialVelocity);
		maxSquishAngle = description.maxSquishAngle;
		minCosSquishAngle = (float) Math.cos(maxSquishAngle);
		data = description.data;
		eventHandler = description.eventHandler;

		setFlag(FLAG_IN_SIMULATION, false);
		setFlag(FLAG_EXISTS, true);

		if (simulation.simulationLock) {
			simulation.movableEntityAddQueue.add(this);
		} else {
			addToSimulation();
		}
	}

	void addToSimulation() {
		setFlag(FLAG_IN_SIMULATION, true);
		simulation.movableEntities.add(this);

		// add to dynamic grid
		Vector2f aabbMin = new Vector2f();
		Vector2f aabbMax = new Vector2f();
		getAabb(aabbMin, aabbMax, simulation.settings.contactThreshold);
		// add to hash grid - movable entities are not activators
		gridEntity = simulation.hashGrid.addEntity(aabbMin, aabbMax, PhysicsSimulation.GRID_MOVABLE_ENTITY_TYPE, false, this);
	}

	// removes this entity from the simulation
	public void remove() {
		checkExists();
		checkMovableEntityLock();
		setFlag(FLAG_EXISTS, false);

		// remove immediately from the grid
		if (gridEntity != null) {
			gridEntity.remove();
			gridEntity = null;
		}

		if (simulation.simulationLock || simulation.raycastLock) {
			// simulation is locked - we can't remove directly
			// add to deletion queue if in the simulation
			if (checkFlag(FLAG_IN_SIMULATION)) {
				simulation.movableEntityDeleteQueue.add(this);
			}
		} else {
			// simulation is not locked - we can directly modify without going through queues
			// if the simulation isn't locked, we should have added this entity by this point
			assert checkFlag(FLAG_IN_SIMULATION);
			// remove from simulation
			simulation.movableEntities.remove(this);
		}
	}

	public boolean exists() {
		return checkFlag(FLAG_EXISTS);
	}

	public void setPosition(Vector2f position) {
		checkExists();
		checkMovableEntityLock();

		if (this.position.equals(position)) {
			return; // no change
		}
		this.position.set(position);

		if (gridEntity != null) {
			// move in dynamic grid
			Vector2f aabbMin = new Vector2f();
			Vector2f aabbMax = new Vector2f();
			getAabb(aabbMin, aabbMax, simulation.settings.contactThreshold);
			// add to hash grid - dynamic entities are activators
			gridEntity.move(aabbMin, aabbMax);
		}
	}

	public void setVelocity(Vector2f velocity) {
		checkExists();
		checkMovableEntityLock();
		this.velocity.set(velocity);
	}

	public void setMaxSquishAngle(float maxSquishAngle) {
		checkExists();
		checkMovableEntityLock();

		if (maxSquishAngle < 0.0f) {
			throw new IllegalArgumentException("Invalid max squish angle");
		}

		this.maxSquishAngle = maxSquishAngle;
		this.minCosSquishAngle = (float) Math.cos(maxSquishAngle);
	}

	public void setData(Object data) {
		checkExists();
		this.data = data;
	}

	public void setEventHandler(EventHandler eventHandler) {
		checkExists();
		this.eventHandler = eventHandler;
	}

	public PhysicsSimulation getSimulation() {
		return checkFlag(FLAG_EXISTS) ? simulation : null;
	}

	public Polygon getShape() {
		return shape;
	}

	public float getRotation() {
		return rotation;
	}

	public Vector2f getPosition() {
		return new Vector2f(position);
	}

	public Vector2f getVelocity() {
		return new Vector2f(velocity);
	}

	public float getMaxSquishAngle() {
		return maxSquishAngle;
	}

	public Object getData() {
		return data;
	}

	public EventHandler getEventHandler() {
		return eventHandler;
	}

	private PhysicsSimulation simulation;

	// fields are package-private to allow access by physics simulator
	Polygon shape;                      // shape for this entity
	float rotation;                     // rotation of this entity
	Vector2f rotationVector;            // vector containing (cos(rotation), sin(rotation))
	Vector2f position;                  // current position
	Vector2f velocity;                  // current velocity
	float maxSquishAngle;               // the angle at which dynamic entities are "squished" between two surfaces
	float minCosSquishAngle;            // cos(maxSquishAngle)
	Object data;                        // user-provided data
	EventHandler eventHandler;          // event handler for this entity
	HashGrid.Entity gridEntity;         // entity in the hash grid

	byte flags = 0;                     // whether this entity is flagged for deletion
	static final byte FLAG_IN_SIMULATION    = 1;    // whether entity has been added to the simulation
	static final byte FLAG_EXISTS           = 2;    // whether entity "exists" to the user
	boolean checkFlag(byte flag) {
		return (flags & flag) != 0;
	}
	void setFlag(byte flag, boolean e) {
		flags = (byte) (e ? (flags | flag) : (flags & ~flag));
	}

	void checkExists() {
		if (!checkFlag(FLAG_EXISTS)) {
			throw new IllegalStateException("Not in simulation");
		}
	}

	void checkMovableEntityLock() {
		if (simulation.movableEntityLock) {
			throw new IllegalArgumentException("Movable entities locked");
		}
	}

	void getAabb(Vector2f aabbMin, Vector2f aabbMax, float expansion) {
		aabbMin.set(Float.MAX_VALUE, Float.MAX_VALUE);
		aabbMax.set(Float.MIN_VALUE, Float.MIN_VALUE);

		// expand AABB
		for (int i = 0; i < shape.getVertexCount(); ++i) {
			Vector2f point = shape.getVertexDirect(i);
			float rotX = point.x() * rotationVector.x() - point.y() * rotationVector.y();
			float rotY = point.x() * rotationVector.y() + point.y() * rotationVector.x();
			aabbMin.x(Math.min(aabbMin.x(), rotX));
			aabbMin.y(Math.min(aabbMin.y(), rotY));
			aabbMax.x(Math.max(aabbMax.x(), rotX));
			aabbMax.y(Math.max(aabbMax.y(), rotY));
		}
		// expand by expansion
		aabbMin.x(aabbMin.x() - expansion + position.x());
		aabbMin.y(aabbMin.y() - expansion + position.y());
		aabbMax.x(aabbMax.x() + expansion + position.x());
		aabbMax.y(aabbMax.y() + expansion + position.y());
	}
}
